/*jslint maxerr: 50, indent: 4, browser: true, bitwise: true, white: true */

/* DOMTokenList v1.2
 *
 * home: http://code.google.com/p/domtokenlist//
 *
 * This type represents a set of space-separated tokens.
 * Commonly returned by HTMLElement.classList, HTMLLinkElement.relList,
 * HTMLAnchorElement.relList or HTMLAreaElement.relList.
 * It is indexed beginning with 0 as with JavaScript arrays.
 * DOMTokenList is always case-sensitive. Written with cross-browser compatibility in mind and
 * does not require any external libraries.>
 *
 * Copyright (C) 2013  Graham Fairweather (a.k.a: Xotic750)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

var tokenList = (function (undef) {
    "use strict";

    /* The space characters, for the purposes of this specification,
     * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab), U+000A LINE FEED (LF),
     * U+000C FORM FEED (FF), and U+000D CARRIAGE RETURN (CR).
     *
     * The White_Space characters are those that have the Unicode property "White_Space"
     * in the Unicode PropList.txt data file.
     *
     * http://www.whatwg.org/specs/web-apps/current-work/multipage/common-microsyntaxes.html#space-character
     */
    var whiteSpaces = " \\t\\n\\f\\r",
        wsSplitRX = new RegExp("[" + whiteSpaces + "]+"),
        wsTrimRX = new RegExp("^[" + whiteSpaces + "]+|[" + whiteSpaces + "]+$", "g"),
        CtrBoolean = true.constructor,
        CtrNumber = (0).constructor,
        CtrString = "".constructor,
        toStringFn = {}.toString;

    function isValid(object) {
        return object !== undef && object !== null;
    }

    function toObject(object) {
        if (!isValid(object)) {
            throw new TypeError("Cannot convert " + object + " to object");
        }

        switch (typeof object) {
        case "boolean":
            return new CtrBoolean(object);
        case "number":
            return new CtrNumber(object);
        case "string":
            return new CtrString(object);
        default:
        }

        return object;
    }

    function toString(string) {
        if (!isValid(string)) {
            string = toStringFn.call(string);
        }

        return toObject(string).toString().replace(wsTrimRX, "");
    }

    function errorIfEmpty(string) {
        if (!string) {
            throw new SyntaxError("Can not be an empty string.");
        }
    }

    function validTokenString(string) {
        errorIfEmpty(string);
        if (string.search(wsSplitRX) !== -1) {
            throw new SyntaxError("May not contain \"White_Space\" characters.");
        }

        return string;
    }

    function isValidIndex(index) {
        return (index >>> 0) === index && index <= 4294967294;
    }

    function isValidIndexRange(index) {
        if (!isValidIndex(index)) {
            throw new RangeError("Index is not of valid range.");
        }

        return index;
    }

    function isValidLength(length) {
        if ((length >>> 0) !== length || length > 4294967295) {
            throw new RangeError("Length is not of valid range.");
        }

        return length;
    }

    function push(array, object) {
        var index = isValidIndexRange(array.length);

        array[index] = object;
        index += 1;
        array.length = index;
    }

    function elementToString(element) {
        if (isValid(element)) {
            return element.toString();
        }

        return "";
    }

    function indexOf(array, searchElement) {
        var length = isValidLength(array.length),
            key = 0;

        while (key < length) {
            if (array.hasOwnProperty(key) && searchElement === array[key]) {
                return key;
            }

            key += 1;
        }

        return -1;
    }

    function augmentToken(token) {
        return validTokenString(toString(token));
    }

    /* This type represents a set of space-separated tokens.
     * Commonly returned by HTMLElement.classList, HTMLLinkElement.relList,
     * HTMLAnchorElement.relList or HTMLAreaElement.relList.
     * It is indexed beginning with 0 as with JavaScript arrays.
     * DOMTokenList is always case-sensitive.
     *
     * https://developer.mozilla.org/en-US/docs/Web/API/DOMTokenList?redirectlocale=en-US&redirectslug=DOM%2FDOMTokenList
     */
    function DOMTokenList(string) {
        if (string === DOMTokenList.prototype.inheritAPI) {
            return;
        }

        this.set(string);
    }

    DOMTokenList.prototype = {
        inheritAPI: {},

        set: function (string) {
            var array = toString(string).split(wsSplitRX),
                length = isValidLength(array.length),
                element,
                i;

            for (i in this) {
                if (this.hasOwnProperty(i) && isValidIndex(i)) {
                    delete this[i];
                }
            }

            i = 0;
            this.length = 0;
            while (i < length) {
                element = array[i];
                if (element) {
                    push(this, element);
                }

                i += 1;
            }
        },

        toString: function () {
            var length = isValidLength(this.length),
                result = elementToString(this[0]),
                k = 1;

            while (k < length) {
                result += (" " + elementToString(this[k]));
                k += 1;
            }

            return result;
        },

        /* Properties
         *
         * length (read-only integer)
         * for cross-browser purposes 'this.length' is read/write
         *
         */
        length: 0,

        /* Methods
         *
         * item ( idx ) - returns an item in the list by its index (or undefined if the number is greater than or equal to the length of the list, prior to Gecko 7.0 returned null)
         * contains ( token ) - return true if the underlying string contains token, otherwise false
         * add ( token ) - adds token to the underlying string
         * remove ( token ) - remove token from the underlying string
         * toggle ( token ) - removes token from string and returns false. If token doesn't exist it's added and the function returns true
         *
         */
        item: function item(position) {
            if (this.hasOwnProperty(position) && isValidIndex(position)) {
                return this[position];
            }

            return undef;
        },

        contains: function (token) {
            return indexOf(this, augmentToken(token)) !== -1;
        },

        add: function (token) {
            token = augmentToken(token);
            if (!this.contains(token)) {
                push(this, token);
            }
        },

        remove: function (token) {
            token = augmentToken(token);
            if (this.contains(token)) {
                var index = indexOf(this, token),
                    length,
                    k,
                    from;

                while (index > -1) {
                    length = this.length;
                    this.length -= 1;
                    if (index < length) {
                        k = index;
                    } else {
                        k = length;
                    }

                    while (k < this.length) {
                        from = (k + 1);
                        if (this.hasOwnProperty(from)) {
                            this[k] = this[from];
                        } else {
                            delete this[k];
                        }

                        k += 1;
                    }

                    k = length;
                    while (k > this.length) {
                        delete this[k - 1];
                        k -= 1;
                    }

                    index = indexOf(this, token);
                }
            }
        },

        toggle: function (token) {
            token = augmentToken(token);
            if (this.contains(token)) {
                this.remove(token);

                return false;
            }

            this.add(token);

            return true;
        }
    };

    function TokenList(object, property) {
        if (object === DOMTokenList.prototype.inheritAPI) {
            return;
        }

        this.setProperty = function () {
            object[property] = DOMTokenList.prototype.toString.call(this);
        };

        this.update = function () {
            DOMTokenList.prototype.set.call(this, object[property]);
        };

        this.update();
    }

    TokenList.prototype = (function () {
        var prototype = new DOMTokenList(DOMTokenList.prototype.inheritAPI),
            overwrite = {
                toString: function () {
                    this.update();

                    return DOMTokenList.prototype.toString.call(this);
                },

                item: function (position) {
                    this.update();

                    return DOMTokenList.prototype.item.call(this, position);
                },

                contains: function (string) {
                    this.update();

                    return DOMTokenList.prototype.contains.call(this, string);
                },

                add: function ( /* token1, ..., tokenN */ ) {
                    var length = arguments.length,
                        i = 0;

                    this.update();
                    while (i < length) {
                        DOMTokenList.prototype.add.call(this, arguments[i]);
                        this.setProperty();
                        i += 1;
                    }
                },

                remove: function ( /* token1, ..., tokenN */ ) {
                    var length = arguments.length,
                        i = 0;

                    this.update();
                    while (i < length) {
                        DOMTokenList.prototype.remove.call(this, arguments[i]);
                        this.setProperty();
                        i += 1;
                    }
                },

                toggle: function (token) {
                    this.update();

                    var state = DOMTokenList.prototype.toggle.call(this, token);

                    this.setProperty();

                    return state;
                }
            },
            i;

        for (i in overwrite) {
            if (overwrite.hasOwnProperty(i)) {
                prototype[i] = overwrite[i];
            }
        }

        return prototype;
    }());

    function addTokenList(object, property, name) {
        object = toObject(object);
        property = toString(property);
        name = toString(name);
        errorIfEmpty(property);
        errorIfEmpty(name);
        if (object.hasOwnProperty(name)) {
            throw new Error("object already has a property named \"" + name + "\"");
        }

        object[name] = new TokenList(object, property);
    }

    return {
        "DOMTokenList": DOMTokenList,
        "TokenList": TokenList,
        "addTokenList": addTokenList
    };
}());
